#ifdef WITH_WEBAPP
import Assistant.WebApp
import Assistant.Threads.WebApp
+#ifdef WITH_PAIRING
+import Assistant.Threads.PairListener
+#endif
#else
import Assistant.Types.UrlRenderer
#endif
then webappthread
else webappthread ++
[ watch commitThread
+#ifdef WITH_WEBAPP
+#ifdef WITH_PAIRING
+ , assist $ pairListenerThread urlrenderer
+#endif
+#endif
, assist pushThread
, assist pushRetryThread
, assist exportThread
--- /dev/null
+{- git-annex assistant pairing remote creation
+ -
+ - Copyright 2012 Joey Hess <id@joeyh.name>
+ -
+ - Licensed under the GNU AGPL version 3 or higher.
+ -}
+
+module Assistant.Pairing.MakeRemote where
+
+import Assistant.Common
+import Assistant.Ssh
+import Assistant.Pairing
+import Assistant.Pairing.Network
+import Assistant.MakeRemote
+import Assistant.Sync
+import Config.Cost
+import Config
+import qualified Types.Remote as Remote
+
+import Network.Socket
+import qualified Data.Text as T
+
+{- Authorized keys are set up before pairing is complete, so that the other
+ - side can immediately begin syncing. -}
+setupAuthorizedKeys :: PairMsg -> OsPath -> IO ()
+setupAuthorizedKeys msg repodir = case validateSshPubKey $ remoteSshPubKey $ pairMsgData msg of
+ Left err -> giveup err
+ Right pubkey -> do
+ absdir <- absPath repodir
+ unlessM (liftIO $ addAuthorizedKeys True absdir pubkey) $
+ giveup "failed setting up ssh authorized keys"
+
+{- When local pairing is complete, this is used to set up the remote for
+ - the host we paired with. -}
+finishedLocalPairing :: PairMsg -> SshKeyPair -> Assistant ()
+finishedLocalPairing msg keypair = do
+ sshdata <- liftIO $ installSshKeyPair keypair =<< pairMsgToSshData msg
+ {- Ensure that we know the ssh host key for the host we paired with.
+ - If we don't, ssh over to get it. -}
+ liftIO $ unlessM (knownHost $ sshHostName sshdata) $
+ void $ sshTranscript
+ [ sshOpt "StrictHostKeyChecking" "no"
+ , sshOpt "NumberOfPasswordPrompts" "0"
+ , "-n"
+ ]
+ (genSshHost (sshHostName sshdata) (sshUserName sshdata))
+ ("git-annex-shell -c configlist " ++ T.unpack (sshDirectory sshdata))
+ Nothing
+ r <- liftAnnex $ addRemote $ makeSshRemote sshdata
+ repo <- liftAnnex $ Remote.getRepo r
+ liftAnnex $ setRemoteCost repo semiExpensiveRemoteCost
+ syncRemote r
+
+{- Mostly a straightforward conversion. Except:
+ - * Determine the best hostname to use to contact the host.
+ - * Strip leading ~/ from the directory name.
+ -}
+pairMsgToSshData :: PairMsg -> IO SshData
+pairMsgToSshData msg = do
+ let d = pairMsgData msg
+ hostname <- liftIO $ bestHostName msg
+ let dir = case remoteDirectory d of
+ ('~':'/':v) -> v
+ v -> v
+ return SshData
+ { sshHostName = T.pack hostname
+ , sshUserName = Just (T.pack $ remoteUserName d)
+ , sshDirectory = T.pack dir
+ , sshRepoName = genSshRepoName hostname (toOsPath dir)
+ , sshPort = 22
+ , needsPubKey = True
+ , sshCapabilities = [GitAnnexShellCapable, GitCapable, RsyncCapable]
+ , sshRepoUrl = Nothing
+ }
+
+{- Finds the best hostname to use for the host that sent the PairMsg.
+ -
+ - If remoteHostName is set, tries to use a .local address based on it.
+ - That's the most robust, if this system supports .local.
+ - Otherwise, looks up the hostname in the DNS for the remoteAddress,
+ - if any. May fall back to remoteAddress if there's no DNS. Ugh. -}
+bestHostName :: PairMsg -> IO HostName
+bestHostName msg = case remoteHostName $ pairMsgData msg of
+ Just h -> do
+ let localname = h ++ ".local"
+ addrs <- catchDefaultIO [] $
+ getAddrInfo Nothing (Just localname) Nothing
+ maybe fallback (const $ return localname) (headMaybe addrs)
+ Nothing -> fallback
+ where
+ fallback = do
+ let a = pairMsgAddr msg
+ let sockaddr = case a of
+ IPv4Addr addr -> SockAddrInet (fromInteger 0) addr
+ IPv6Addr addr -> SockAddrInet6 (fromInteger 0) 0 addr 0
+ fromMaybe (showAddr a)
+ <$> catchDefaultIO Nothing
+ (fst <$> getNameInfo [] True False sockaddr)
--- /dev/null
+{- git-annex assistant pairing network code
+ -
+ - All network traffic is sent over multicast UDP. For reliability,
+ - each message is repeated until acknowledged. This is done using a
+ - thread, that gets stopped before the next message is sent.
+ -
+ - Copyright 2012 Joey Hess <id@joeyh.name>
+ -
+ - Licensed under the GNU AGPL version 3 or higher.
+ -}
+
+module Assistant.Pairing.Network where
+
+import Assistant.Common
+import Assistant.Pairing
+import Assistant.DaemonStatus
+import Utility.ThreadScheduler
+import Utility.Verifiable
+
+import Network.Multicast
+import Network.Info
+import Network.Socket
+import qualified Network.Socket.ByteString as B
+import qualified Data.ByteString.UTF8 as BU8
+import qualified Data.Map as M
+import Control.Concurrent
+
+{- This is an arbitrary port in the dynamic port range, that could
+ - conceivably be used for some other broadcast messages.
+ - If so, hope they ignore the garbage from us; we'll certainly
+ - ignore garbage from them. Wild wild west. -}
+pairingPort :: PortNumber
+pairingPort = 55556
+
+{- Goal: Reach all hosts on the same network segment.
+ - Method: Use same address that avahi uses. Other broadcast addresses seem
+ - to not be let through some routers. -}
+multicastAddress :: AddrClass -> HostName
+multicastAddress IPv4AddrClass = "224.0.0.251"
+multicastAddress IPv6AddrClass = "ff02::fb"
+
+{- Multicasts a message repeatedly on all interfaces, with a 2 second
+ - delay between each transmission. The message is repeated forever
+ - unless a number of repeats is specified.
+ -
+ - The remoteHostAddress is set to the interface's IP address.
+ -
+ - Note that new sockets are opened each time. This is hardly efficient,
+ - but it allows new network interfaces to be used as they come up.
+ - On the other hand, the expensive DNS lookups are cached.
+ -}
+multicastPairMsg :: Maybe Int -> Secret -> PairData -> PairStage -> IO ()
+multicastPairMsg repeats secret pairdata stage = go M.empty repeats
+ where
+ go _ (Just 0) = noop
+ go cache n = do
+ addrs <- activeNetworkAddresses
+ let cache' = updatecache cache addrs
+ mapM_ (sendinterface cache') addrs
+ threadDelaySeconds (Seconds 2)
+ go cache' $ pred <$> n
+ {- The multicast library currently chokes on ipv6 addresses. -}
+ sendinterface _ (IPv6Addr _) = noop
+ sendinterface cache i = void $ tryIO $
+ withSocketsDo $ bracket setup cleanup use
+ where
+ setup = multicastSender (multicastAddress IPv4AddrClass) pairingPort
+ cleanup (sock, _) = close sock -- FIXME does not work
+ use (sock, addr) = do
+ setInterface sock (showAddr i)
+ maybe noop
+ (\s -> void $ B.sendTo sock (BU8.fromString s) addr)
+ (M.lookup i cache)
+ updatecache cache [] = cache
+ updatecache cache (i:is)
+ | M.member i cache = updatecache cache is
+ | otherwise = updatecache (M.insert i (show $ mkmsg i) cache) is
+ mkmsg addr = PairMsg $
+ mkVerifiable (stage, pairdata, addr) secret
+
+startSending :: PairingInProgress -> PairStage -> (PairStage -> IO ()) -> Assistant ()
+startSending pip stage sender = do
+ a <- asIO start
+ void $ liftIO $ forkIO a
+ where
+ start = do
+ tid <- liftIO myThreadId
+ let pip' = pip { inProgressPairStage = stage, inProgressThreadId = Just tid }
+ oldpip <- modifyDaemonStatus $
+ \s -> (s { pairingInProgress = Just pip' }, pairingInProgress s)
+ maybe noop stopold oldpip
+ liftIO $ sender stage
+ stopold = maybe noop (liftIO . killThread) . inProgressThreadId
+
+stopSending :: PairingInProgress -> Assistant ()
+stopSending pip = do
+ maybe noop (liftIO . killThread) $ inProgressThreadId pip
+ modifyDaemonStatus_ $ \s -> s { pairingInProgress = Nothing }
+
+class ToSomeAddr a where
+ toSomeAddr :: a -> SomeAddr
+
+instance ToSomeAddr IPv4 where
+ toSomeAddr (IPv4 a) = IPv4Addr a
+
+instance ToSomeAddr IPv6 where
+ toSomeAddr (IPv6 o1 o2 o3 o4) = IPv6Addr (o1, o2, o3, o4)
+
+showAddr :: SomeAddr -> HostName
+showAddr (IPv4Addr a) = show $ IPv4 a
+showAddr (IPv6Addr (o1, o2, o3, o4)) = show $ IPv6 o1 o2 o3 o4
+
+activeNetworkAddresses :: IO [SomeAddr]
+activeNetworkAddresses = filter (not . all (`elem` "0.:") . showAddr)
+ . concatMap (\ni -> [toSomeAddr $ ipv4 ni, toSomeAddr $ ipv6 ni])
+ <$> getNetworkInterfaces
+
+{- A human-visible description of the repository being paired with.
+ - Note that the repository's description is not shown to the user, because
+ - it could be something like "my repo", which is confusing when pairing
+ - with someone else's repo. However, this has the same format as the
+ - default description of a repo. -}
+pairRepo :: PairMsg -> String
+pairRepo msg = concat
+ [ remoteUserName d
+ , "@"
+ , fromMaybe (showAddr $ pairMsgAddr msg) (remoteHostName d)
+ , ":"
+ , remoteDirectory d
+ ]
+ where
+ d = pairMsgData msg
--- /dev/null
+{- git-annex assistant thread to listen for incoming pairing traffic
+ -
+ - Copyright 2012 Joey Hess <id@joeyh.name>
+ -
+ - Licensed under the GNU AGPL version 3 or higher.
+ -}
+
+{-# LANGUAGE OverloadedStrings #-}
+
+module Assistant.Threads.PairListener where
+
+import Assistant.Common
+import Assistant.Pairing
+import Assistant.Pairing.Network
+import Assistant.Pairing.MakeRemote
+import Assistant.WebApp (UrlRenderer)
+import Assistant.WebApp.Types
+import Assistant.Alert
+import Assistant.DaemonStatus
+import Utility.ThreadScheduler
+import Git
+
+import Network.Multicast
+import Network.Socket
+import qualified Data.ByteString as B
+import qualified Data.ByteString.UTF8 as BU8
+import qualified Network.Socket.ByteString as B
+import qualified Data.Text as T
+
+pairListenerThread :: UrlRenderer -> NamedThread
+pairListenerThread urlrenderer = namedThread "PairListener" $ do
+ listener <- asIO1 $ go [] []
+ liftIO $ withSocketsDo $
+ runEvery (Seconds 60) $ void $ tryIO $
+ listener =<< getsock
+ where
+ {- Note this can crash if there's no network interface,
+ - or only one like lo that doesn't support multicast. -}
+ getsock = multicastReceiver (multicastAddress IPv4AddrClass) pairingPort
+
+ go reqs cache sock = liftIO (getmsg sock B.empty) >>= \msg -> case readish (BU8.toString msg) of
+ Nothing -> go reqs cache sock
+ Just m -> do
+ debug ["received", show msg]
+ (pip, verified) <- verificationCheck m
+ =<< (pairingInProgress <$> getDaemonStatus)
+ let wrongstage = maybe False (\p -> pairMsgStage m <= inProgressPairStage p) pip
+ let fromus = maybe False (\p -> remoteSshPubKey (pairMsgData m) == remoteSshPubKey (inProgressPairData p)) pip
+ case (wrongstage, fromus, checkSane (pairMsgData m), pairMsgStage m) of
+ (_, True, _, _) -> do
+ debug ["ignoring message that looped back"]
+ go reqs cache sock
+ (_, _, False, _) -> do
+ liftAnnex $ warning $ UnquotedString $
+ "illegal control characters in pairing message; ignoring (" ++ show (pairMsgData m) ++ ")"
+ go reqs cache sock
+ -- PairReq starts a pairing process, so a
+ -- new one is always heeded, even if
+ -- some other pairing is in process.
+ (_, _, _, PairReq) -> if m `elem` reqs
+ then go reqs (invalidateCache m cache) sock
+ else do
+ pairReqReceived verified urlrenderer m
+ go (m:take 10 reqs) (invalidateCache m cache) sock
+ (True, _, _, _) -> do
+ debug
+ ["ignoring out of order message"
+ , show (pairMsgStage m)
+ , "expected"
+ , show (succ . inProgressPairStage <$> pip)
+ ]
+ go reqs cache sock
+ (_, _, _, PairAck) -> do
+ cache' <- pairAckReceived verified pip m cache
+ go reqs cache' sock
+ (_,_ , _, PairDone) -> do
+ pairDoneReceived verified pip m
+ go reqs cache sock
+
+ {- As well as verifying the message using the shared secret,
+ - check its UUID against the UUID we have stored. If
+ - they're the same, someone is sending bogus messages,
+ - which could be an attempt to brute force the shared secret. -}
+ verificationCheck _ Nothing = return (Nothing, False)
+ verificationCheck m (Just pip)
+ | not verified && sameuuid = do
+ liftAnnex $ warning
+ "detected possible pairing brute force attempt; disabled pairing"
+ stopSending pip
+ return (Nothing, False)
+ | otherwise = return (Just pip, verified && sameuuid)
+ where
+ verified = verifiedPairMsg m pip
+ sameuuid = pairUUID (inProgressPairData pip) == pairUUID (pairMsgData m)
+
+ {- PairReqs invalidate the cache of recently finished pairings.
+ - This is so that, if a new pairing is started with the
+ - same secret used before, a bogus PairDone is not sent. -}
+ invalidateCache msg = filter (not . verifiedPairMsg msg)
+
+ getmsg sock c = do
+ (msg, _) <- B.recvFrom sock chunksz
+ if B.length msg < chunksz
+ then return $ c <> msg
+ else getmsg sock $ c <> msg
+ where
+ chunksz = 1024
+
+{- Show an alert when a PairReq is seen. -}
+pairReqReceived :: Bool -> UrlRenderer -> PairMsg -> Assistant ()
+pairReqReceived True _ _ = noop -- ignore our own PairReq
+pairReqReceived False urlrenderer msg = do
+ button <- mkAlertButton True (T.pack "Respond") urlrenderer (FinishLocalPairR msg)
+ void $ addAlert $ pairRequestReceivedAlert repo button
+ where
+ repo = pairRepo msg
+
+{- When a verified PairAck is seen, a host is ready to pair with us, and has
+ - already configured our ssh key. Stop sending PairReqs, finish the pairing,
+ - and send a single PairDone. -}
+pairAckReceived :: Bool -> Maybe PairingInProgress -> PairMsg -> [PairingInProgress] -> Assistant [PairingInProgress]
+pairAckReceived True (Just pip) msg cache = do
+ stopSending pip
+ repodir <- repoPath <$> liftAnnex gitRepo
+ liftIO $ setupAuthorizedKeys msg repodir
+ finishedLocalPairing msg (inProgressSshKeyPair pip)
+ startSending pip PairDone $ multicastPairMsg
+ (Just 1) (inProgressSecret pip) (inProgressPairData pip)
+ return $ pip : take 10 cache
+{- A stale PairAck might also be seen, after we've finished pairing.
+ - Perhaps our PairDone was not received. To handle this, we keep
+ - a cache of recently finished pairings, and re-send PairDone in
+ - response to stale PairAcks for them. -}
+pairAckReceived _ _ msg cache = do
+ let pips = filter (verifiedPairMsg msg) cache
+ unless (null pips) $
+ forM_ pips $ \pip ->
+ startSending pip PairDone $ multicastPairMsg
+ (Just 1) (inProgressSecret pip) (inProgressPairData pip)
+ return cache
+
+{- If we get a verified PairDone, the host has accepted our PairAck, and
+ - has paired with us. Stop sending PairAcks, and finish pairing with them.
+ -
+ - TODO: Should third-party hosts remove their pair request alert when they
+ - see a PairDone?
+ - Complication: The user could have already clicked on the alert and be
+ - entering the secret. Would be better to start a fresh pair request in this
+ - situation.
+ -}
+pairDoneReceived :: Bool -> Maybe PairingInProgress -> PairMsg -> Assistant ()
+pairDoneReceived False _ _ = noop -- not verified
+pairDoneReceived True Nothing _ = noop -- not in progress
+pairDoneReceived True (Just pip) msg = do
+ stopSending pip
+ finishedLocalPairing msg (inProgressSshKeyPair pip)
import Assistant.Pairing
import Assistant.WebApp.Common
import Annex.UUID
+#ifdef WITH_PAIRING
+import Assistant.DaemonStatus
+import Assistant.Pairing.MakeRemote
+import Assistant.Pairing.Network
+import Assistant.Ssh
+import Utility.Verifiable
+#endif
import Utility.UserInfo
import Utility.Tor
import Utility.Su
import qualified Data.Map as M
import qualified Data.Text as T
+#ifdef WITH_PAIRING
+import qualified Data.Text.Encoding as T
+import qualified Data.ByteString as B
+import Data.Char
+import qualified Control.Exception as E
+import Control.Concurrent
+#endif
import Control.Concurrent.STM hiding (check)
getStartWormholePairFriendR :: Handler Html
$(widgetFile "configurators/needmagicwormhole")
)
+{- Starts local pairing. -}
+getStartLocalPairR :: Handler Html
+getStartLocalPairR = postStartLocalPairR
+postStartLocalPairR :: Handler Html
+#ifdef WITH_PAIRING
+postStartLocalPairR = promptSecret Nothing $
+ startLocalPairing PairReq noop pairingAlert Nothing
+#else
+postStartLocalPairR = noLocalPairing
+
+noLocalPairing :: Handler Html
+noLocalPairing = noPairing "local"
+#endif
+
+{- Runs on the system that responds to a local pair request; sets up the ssh
+ - authorized key first so that the originating host can immediately sync
+ - with us. -}
+getFinishLocalPairR :: PairMsg -> Handler Html
+getFinishLocalPairR = postFinishLocalPairR
+postFinishLocalPairR :: PairMsg -> Handler Html
+#ifdef WITH_PAIRING
+postFinishLocalPairR msg = promptSecret (Just msg) $ \_ secret -> do
+ repodir <- liftH $ repoPath <$> liftAnnex gitRepo
+ liftIO $ setup repodir
+ startLocalPairing PairAck (cleanup repodir) alert uuid "" secret
+ where
+ alert = pairRequestAcknowledgedAlert (pairRepo msg) . Just
+ setup repodir = setupAuthorizedKeys msg repodir
+ cleanup repodir = removeAuthorizedKeys True repodir $
+ remoteSshPubKey $ pairMsgData msg
+ uuid = Just $ pairUUID $ pairMsgData msg
+#else
+postFinishLocalPairR _ = noLocalPairing
+#endif
+
+getRunningLocalPairR :: SecretReminder -> Handler Html
+#ifdef WITH_PAIRING
+getRunningLocalPairR s = pairPage $ do
+ let secret = fromSecretReminder s
+ $(widgetFile "configurators/pairing/local/inprogress")
+#else
+getRunningLocalPairR _ = noLocalPairing
+#endif
+
+#ifdef WITH_PAIRING
+
+{- Starts local pairing, at either the PairReq (initiating host) or
+ - PairAck (responding host) stage.
+ -
+ - Displays an alert, and starts a thread sending the pairing message,
+ - which will continue running until the other host responds, or until
+ - canceled by the user. If canceled by the user, runs the oncancel action.
+ -
+ - Redirects to the pairing in progress page.
+ -}
+startLocalPairing :: PairStage -> IO () -> (AlertButton -> Alert) -> Maybe UUID -> Text -> Secret -> Widget
+startLocalPairing stage oncancel alert muuid displaysecret secret = do
+ urlrender <- liftH getUrlRender
+ reldir <- fromJust . relDir <$> liftH getYesod
+
+ sendrequests <- liftAssistant $ asIO2 $ mksendrequests urlrender
+ {- Generating a ssh key pair can take a while, so do it in the
+ - background. -}
+ thread <- liftAssistant $ asIO $ do
+ keypair <- liftIO $ genSshKeyPair
+ let pubkey = either giveup id $ validateSshPubKey $ sshPubKey keypair
+ pairdata <- liftIO $ PairData
+ <$> getHostname
+ <*> (either giveup id <$> myUserName)
+ <*> pure reldir
+ <*> pure pubkey
+ <*> (maybe genUUID return muuid)
+ let sender = multicastPairMsg Nothing secret pairdata
+ let pip = PairingInProgress secret Nothing keypair pairdata stage
+ startSending pip stage $ sendrequests sender
+ void $ liftIO $ forkIO thread
+
+ liftH $ redirect $ RunningLocalPairR $ toSecretReminder displaysecret
+ where
+ {- Sends pairing messages until the thread is killed,
+ - and shows an activity alert while doing it.
+ -
+ - The cancel button returns the user to the DashboardR. This is
+ - not ideal, but they have to be sent somewhere, and could
+ - have been on a page specific to the in-process pairing
+ - that just stopped, so can't go back there.
+ -}
+ mksendrequests urlrender sender _stage = do
+ tid <- liftIO myThreadId
+ let selfdestruct = AlertButton
+ { buttonLabel = "Cancel"
+ , buttonPrimary = True
+ , buttonUrl = urlrender DashboardR
+ , buttonAction = Just $ const $ do
+ oncancel
+ killThread tid
+ }
+ alertDuring (alert selfdestruct) $ liftIO $ do
+ _ <- E.try (sender stage) :: IO (Either E.SomeException ())
+ return ()
+
+data InputSecret = InputSecret { secretText :: Maybe Text }
+
+{- If a PairMsg is passed in, ensures that the user enters a secret
+ - that can validate it. -}
+promptSecret :: Maybe PairMsg -> (Text -> Secret -> Widget) -> Handler Html
+promptSecret msg cont = pairPage $ do
+ ((result, form), enctype) <- liftH $
+ runFormPostNoToken $ renderBootstrap3 bootstrapFormLayout $
+ InputSecret <$> aopt textField (bfs "Secret phrase") Nothing
+ case result of
+ FormSuccess v -> do
+ let rawsecret = fromMaybe "" $ secretText v
+ let secret = toSecret rawsecret
+ case msg of
+ Nothing -> case secretProblem secret of
+ Nothing -> cont rawsecret secret
+ Just problem ->
+ showform form enctype $ Just problem
+ Just m ->
+ if verify (fromPairMsg m) secret
+ then cont rawsecret secret
+ else showform form enctype $ Just
+ "That's not the right secret phrase."
+ _ -> showform form enctype Nothing
+ where
+ showform form enctype mproblem = do
+ let start = isNothing msg
+ let badphrase = isJust mproblem
+ let problem = fromMaybe "" mproblem
+ let (username, hostname) = maybe ("", "")
+ (\(_, v, a) -> (T.pack $ remoteUserName v, T.pack $ fromMaybe (showAddr a) (remoteHostName v)))
+ (verifiableVal . fromPairMsg <$> msg)
+ u <- liftIO myUserName
+ let sameusername = Right username == (T.pack <$> u)
+ $(widgetFile "configurators/pairing/local/prompt")
+
+{- This counts unicode characters as more than one character,
+ - but that's ok; they *do* provide additional entropy. -}
+secretProblem :: Secret -> Maybe Text
+secretProblem s
+ | B.null s = Just "The secret phrase cannot be left empty. (Remember that punctuation and white space is ignored.)"
+ | B.length s < 6 = Just "Enter a longer secret phrase, at least 6 characters, but really, a phrase is best! This is not a password you'll need to enter every day."
+ | s == toSecret sampleQuote = Just "Speaking of foolishness, don't paste in the example I gave. Enter a different phrase, please!"
+ | otherwise = Nothing
+
+toSecret :: Text -> Secret
+toSecret s = T.encodeUtf8 $ T.toLower $ T.filter isAlphaNum s
+
+{- From Dickens -}
+sampleQuote :: Text
+sampleQuote = T.unwords
+ [ "It was the best of times,"
+ , "it was the worst of times,"
+ , "it was the age of wisdom,"
+ , "it was the age of foolishness."
+ ]
+
+#else
+
+#endif
+
pairPage :: Widget -> Handler Html
pairPage = page "Pairing" (Just Configuration)
+
+noPairing :: Text -> Handler Html
+noPairing pairingtype = pairPage $
+ $(widgetFile "configurators/pairing/disabled")
/config/repository/add/cloud/IA AddIAR GET POST
/config/repository/add/cloud/glacier AddGlacierR GET POST
+/config/repository/pair/local/start StartLocalPairR GET POST
+/config/repository/pair/local/running/#SecretReminder RunningLocalPairR GET
+/config/repository/pair/local/finish/#PairMsg FinishLocalPairR GET POST
+
/config/repository/pair/wormhole/start/self StartWormholePairSelfR GET
/config/repository/pair/wormhole/start/friend StartWormholePairFriendR GET
/config/repository/pair/wormhole/prepare/#PairingWith PrepareWormholePairR GET
#else
#warning Building without the webapp.
#endif
+#ifdef WITH_PAIRING
+ , "Pairing"
+#else
+#warning Building without local pairing.
+#endif
#ifdef WITH_INOTIFY
, "Inotify"
#endif
-git-annex (10.20250930) UNRELEASED; urgency=medium
-
- * webapp: Remove support for local pairing; use wormhole pairing instead.
- * git-annex.cabal: Removed pairing build flag, and no longer depends
- on network-multicast or network-info.
-
- -- Joey Hess <id@joeyh.name> Mon, 29 Sep 2025 12:35:51 -0400
-
git-annex (10.20250929) upstream; urgency=medium
* enableremote: Allow type= to be provided when it does not change the
libghc-http-client-restricted-dev,
libghc-blaze-builder-dev,
libghc-crypto-api-dev,
+ libghc-network-multicast-dev,
+ libghc-network-info-dev [linux-any kfreebsd-any],
libghc-safesemaphore-dev,
libghc-async-dev,
libghc-monad-logger-dev,
The git-annex assistant comes as part of git-annex.
See [[install]] to get it installed.
+See the [[release_notes]] for an overview of the status, and upgrade
+instructions.
+
## intro screencast
[[!inline feeds=no template=bare pages=videos/git-annex_assistant_lan]]
* [[Basic usage|quickstart]]
* [[Android documentation|/Android]]
+* Want to make two nearby computers share the same synchronised folder?
+ Follow the [[local_pairing_walkthrough]].
* Want to share files with a friend? Follow the
[[share_with_a_friend_walkthrough]].
* Want to archive data to a drive or the cloud?
--- /dev/null
+So you have two computers in the same building, and you want them to share
+the same synchronised folder, communicating directly with each other.
+
+This is incredibly easy to set up with the git annex assistant.
+
+Let's say the two computers are your computer and your friend's computer.
+We'll start on your computer, where you open up your git annex dashboard.
+
+[[!img addrepository.png alt="Add another repository button"]]
+
+`*click*`
+
+[[!img pairing.png alt="Pair with another computer"]]
+
+`*click*`
+
+Now the hard bit. You have to think up a secret phrase, and type it in,
+(and perhaps get the spelling correct).
+
+[[!img secret.png alt="Enter secret phrase"]]
+
+Now your computer is in pairing mode. When your friend looks at her git
+annex dashboard, she sees something like this.
+
+[[!img pairrequest.png alt="Pair request"]]
+
+`*click*`
+
+[[!img secretempty.png alt="Enter same secret phrase"]]
+
+Now it's up to you to let her know what the secret is. As soon as she
+enters it, both your computers will be paired, and will begin to sync their
+git-annex folders. Just like that you can share files.
+
+----
+
+## Requirements
+
+For local pairing to work, you must have sshd (ssh server daemon) installed and working on all machines involved. That means you must allow at least local connections to sshd. On most Linux distributions, sshd is packaged in either openssh (openSUSE) or openssh-server (Debian).
+
+It is highly recommended that you disable root login, disable password login to sshd and just enable key based authentication instead. No one will be able to login without your key.
+
+To disable root, after installing sshd, edit the sshd config (usually /etc/ssh/sshd_config file) and disable root login by adding:
+
+ PermitRootLogin no
+
+Restart sshd. See man sshd_config for details.
+
+To disable password login and enable key based authentication, edit the sshd config (just like above) by uncommenting and changing the following options:
+
+ ChallengeResponseAuthentication no
+ PasswordAuthentication no
+ UsePAM no
+
+ PubkeyAuthentication yes
+
+Restart sshd. See man sshd_config for details.
+
+You can also restrict login to your local network only (not allow internet users from trying to log into your computer). Edit the hosts.deny file (usually /etc/hosts.deny) by adding the following:
+
+ sshd : ALL EXCEPT LOCAL
+
+Do note that restricting login to your local network may or may not block git-annex. Also note that this will not work on Mac OSX because Apple decided to disable this feature and replace it with a crippled version made by Apple.
+
+## Tips
+
+Something to keep in mind, especially if pairing doesn't seem to be
+working, is that the two computers need to be on the same network for this
+pairing process to work. Sometimes a building will have more than one
+network inside it, and you'll need to connect them both to the same one.
+Make sure the wireless network name is the same, or that they're both
+plugged into the same router.
+
+Also, the file sharing set up by this pairing only works when both
+computers are on the same network. If you go on a trip, any files you
+edit will not be visible to your friend until you get back.
+
+To get around this, you'll often also want to set up
+[[tor_pairing|share_with_a_friend_walkthrough]] too,
+which they can use to exchange files while away.
+
+And also, you can pair with as many other computers as you like, not just
+one!
+
+## What does pairing actually do behind the scenes?
+
+It ensures that both repositories have correctly configured
+[[remotes|walkthrough/adding_a_remote]] pointing to each other.
+If you have already configured this manually, you do not need to
+perform pairing.
[[!inline feeds=no template=bare pages=videos/git-annex_assistant_remote_sharing]]
You can add even more computers using the same method shown here.
+
+----
+
+If you have a laptop that is sometimes near another computer, you can
+speed up file transfers when it is by also connecting it using the
+[[local_pairing_walkthrough]].
"""]]
[[!taglink moreinfo]]
-
-> This feature has been removed. [[done]] --[[Joey]]
network protocol used by the webapp's pairing interface is baked into the
assistant even when the webapp is not being used. And is not otherwise used
in git-annex, and has had at least one security issue in the past.
-(Update: That has been removed from the webapp now.)
The git-annex binary also ends up significantly larger due to containing
the webapp. And removing it deletes 28 thousand lines of code from
templates/configurators/newrepository/first.hamlet
templates/configurators/newrepository/combine.hamlet
templates/configurators/enablewebdav.hamlet
+ templates/configurators/pairing/local/inprogress.hamlet
+ templates/configurators/pairing/local/prompt.hamlet
templates/configurators/pairing/wormhole/prompt.hamlet
templates/configurators/pairing/wormhole/start.hamlet
+ templates/configurators/pairing/disabled.hamlet
templates/configurators/addglacier.hamlet
templates/configurators/fsck.cassius
templates/configurators/edit/nonannexremote.hamlet
Description: Enable git-annex assistant, webapp, and watch command
Default: True
+Flag Pairing
+ Description: Enable pairing
+
Flag Production
Description: Enable production build (slower build; faster binary)
Assistant.Monad
Assistant.NamedThread
Assistant.Pairing
+ Assistant.Pairing.MakeRemote
+ Assistant.Pairing.Network
Assistant.Pushes
Assistant.RemoteControl
Assistant.Repair
Assistant.Threads.Merger
Assistant.Threads.MountWatcher
Assistant.Threads.NetWatcher
+ Assistant.Threads.PairListener
Assistant.Threads.ProblemFixer
Assistant.Threads.Pusher
Assistant.Threads.RemoteControl
CPP-Options: -DWITH_DBUS -DWITH_DESKTOP_NOTIFY -DWITH_DBUS_NOTIFICATIONS
Other-Modules: Utility.DBus
+ if flag(Pairing)
+ Build-Depends: network-multicast, network-info
+ CPP-Options: -DWITH_PAIRING
+
if flag(TorrentParser)
Build-Depends: torrent (>= 10000.0.0)
CPP-Options: -DWITH_TORRENTPARSER
production: true
parallelbuild: true
assistant: true
+ pairing: true
torrentparser: true
magicmime: false
dbus: false
^{makeWormholePairing}
+<h3>
+ <a href="@{StartLocalPairR}">
+ <span .glyphicon .glyphicon-plus-sign>
+ \ Local computer
+<p>
+ Pair with a computer to keep files in sync quickly, #
+ over your local network.
+
<h3>
<a href="@{NewRepositoryR}">
<span .glyphicon .glyphicon-plus-sign>
--- /dev/null
+<div .col-sm-9>
+ <div .content-box>
+ <h2>
+ not supported
+ <p>
+ This build of git-annex does not support #{pairingtype} pairing. Sorry!
--- /dev/null
+<div .col-sm-9>
+ <div .content-box>
+ <h2>
+ Pairing in progress ..
+ $if T.null secret
+ <p>
+ You do not need to leave this page open; pairing will finish #
+ automatically.
+ $else
+ <p>
+ Now you should either go tell the owner of the computer you want to pair #
+ with the secret phrase you selected ("#{secret}"), or go enter it into #
+ the computer you want to pair with.
+ <p>
+ You do not need to leave this page open; pairing will finish automatically #
+ as soon as the secret phrase is entered into the other computer.
+ <p>
+ If you're not seeing a pair request on the other computer, try moving #
+ it to the same switch or wireless network as this one.
--- /dev/null
+<div .col-sm-9>
+ <div .content-box>
+ <h2>
+ Pairing with a local computer
+ <p>
+ $if start
+ Pair with a computer on your local network (or VPN), and the #
+ two git annex repositories will be combined into one, with changes #
+ kept in sync between them.
+ $else
+ Pairing with #{username}@#{hostname} will combine your two git annex #
+ repositories into one, allowing you to share files.
+ <p>
+ $if start
+ For security, enter a secret phrase. To verify that you're pairing #
+ with the right computer, its git-annex webapp will then prompt for the #
+ same secret phrase. That's all this secret phrase will be used for.
+ $else
+ $if sameusername
+ For security, you need to enter the same secret phrase that was #
+ entered on #{hostname} when the pairing was started.
+ $else
+ For security, a secret phrase has been selected, which you need #
+ to enter here to finish the pairing. If you don't know the #
+ phrase, go ask #{username} ...
+ $if badphrase
+ <div .alert .alert-danger>
+ <span .glyphicon .glyphicon-warning-sign>
+ \ #{problem}
+ <p>
+ <form method="post" .form-horizontal enctype=#{enctype}>
+ <fieldset>
+ ^{form}
+ ^{webAppFormAuthToken}
+ <div .form-group>
+ <div .col-sm-10 .col-sm-offset-2>
+ <button .btn .btn-primary type=submit>
+ $if start
+ Start pairing
+ $else
+ Finish pairing
+ <div .alert .alert-info>
+ $if start
+ <p>
+ A good secret phrase is reasonably long. You'll only #
+ type it a few times. Only letters and numbers matter; #
+ punctuation and white space is ignored.
+ <p>
+ A quotation is one good choice, something like: #
+ "#{sampleQuote}"
+ $else
+ Only letters and numbers matter; punctuation and spaces are #
+ ignored.